Hệ thống xếp lịch học tín chỉ cho sinh viên CNTT trên PHP & MySQL
111.095 lượt xem;
- ci_input.php
- project /
1 <?php
2 /**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP 4.3.2 or newer
6 *
7 * @package CodeIgniter
8 * @author ExpressionEngine Dev Team
9 * @copyright Copyright (c) 2008 - 2009, EllisLab, Inc.
10 * @license http://codeigniter.com/user_guide/license.html
11 * @link http://codeigniter.com
12 * @since Version 1.0
13 * @filesource
14 */
15
16 // ------------------------------------------------------------------------
17
18 /**
19 * Input Class
20 *
21 * Pre-processes global input data for security
22 *
23 * @package CodeIgniter
24 * @subpackage Libraries
25 * @category Input
26 * @author ExpressionEngine Dev Team
27 * @link http://codeigniter.com/user_guide/libraries/input.html
28 */
29 class CI_Input {
30 var $use_xss_clean = FALSE;
31 var $xss_hash = '';
32 var $ip_address = FALSE;
33 var $user_agent = FALSE;
34 var $allow_get_array = FALSE;
35 var $charset = 'iso-8859-1';
36
37 /* never allowed, string replacement */
38 var $never_allowed_str = array(
39 'document.cookie' => '[removed]',
40 'document.write' => '[removed]',
41 '.parentNode' => '[removed]',
42 '.innerHTML' => '[removed]',
43 'window.location' => '[removed]',
44 '-moz-binding' => '[removed]',
45 '<!--' => '<!--',
46 '-->' => '-->',
47 '<![CDATA[' => '<![CDATA['
48 );
49 /* never allowed, regex replacement */
50 var $never_allowed_regex = array(
51 "javascript\s*:" => '[removed]',
52 "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE
53 "vbscript\s*:" => '[removed]', // IE, surprise!
54 "Redirect\s+302" => '[removed]'
55 );
56
57
58 function __construct(){
59 $this->CI_Input();
60 }
61
62 /**
63 * Constructor
64 *
65 * Sets whether to globally enable the XSS processing
66 * and whether to allow the $_GET array
67 *
68 * @access public
69 */
70 function CI_Input()
71 {
72 $this->use_xss_clean = FALSE;
73 $this->allow_get_array = TRUE;
74 // $this->_sanitize_globals();
75 }
76
77 // --------------------------------------------------------------------
78
79 /**
80 * Sanitize Globals
81 *
82 * This function does the following:
83 *
84 * Unsets $_GET data (if query strings are not enabled)
85 *
86 * Unsets all globals if register_globals is enabled
87 *
88 * Standardizes newline characters to \n
89 *
90 * @access private
91 * @return void
92 */
93 function _sanitize_globals()
94 {
95 // Would kind of be "wrong" to unset any of these GLOBALS
96 $protected = array('_SERVER', '_GET', '_POST', '_FILES', '_REQUEST', '_SESSION', '_ENV', 'GLOBALS', 'HTTP_RAW_POST_DATA',
97 'system_folder', 'application_folder', 'BM', 'EXT', 'CFG', 'URI', 'RTR', 'OUT', 'IN');
98
99 // Unset globals for security.
100 // This is effectively the same as register_globals = off
101 foreach (array($_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, $_ENV, (isset($_SESSION) && is_array($_SESSION)) ? $_SESSION : array()) as $global)
102 {
103 if ( ! is_array($global))
104 {
105 if ( ! in_array($global, $protected))
106 {
107 unset($GLOBALS[$global]);
108 }
109 }
110 else
111 {
112 foreach ($global as $key => $val)
113 {
114 if ( ! in_array($key, $protected))
115 {
116 unset($GLOBALS[$key]);
117 }
118
119 if (is_array($val))
120 {
121 foreach($val as $k => $v)
122 {
123 if ( ! in_array($k, $protected))
124 {
125 unset($GLOBALS[$k]);
126 }
127 }
128 }
129 }
130 }
131 }
132
133 // Is $_GET data allowed? If not we'll set the $_GET to an empty array
134 if ($this->allow_get_array == FALSE)
135 {
136 $_GET = array();
137 }
138 else
139 {
140 $_GET = $this->_clean_input_data($_GET);
141 }
142
143 // Clean $_POST Data
144 $_POST = $this->_clean_input_data($_POST);
145
146 // Clean $_COOKIE Data
147 // Also get rid of specially treated cookies that might be set by a server
148 // or silly application, that are of no use to a CI application anyway
149 // but that when present will trip our 'Disallowed Key Characters' alarm
150 // http://www.ietf.org/rfc/rfc2109.txt
151 // note that the key names below are single quoted strings, and are not PHP variables
152 unset($_COOKIE['$Version']);
153 unset($_COOKIE['$Path']);
154 unset($_COOKIE['$Domain']);
155 $_COOKIE = $this->_clean_input_data($_COOKIE);
156
157 }
158
159 // --------------------------------------------------------------------
160
161 /**
162 * Clean Input Data
163 *
164 * This is a helper function. It escapes data and
165 * standardizes newline characters to \n
166 *
167 * @access private
168 * @param string
169 * @return string
170 */
171 function _clean_input_data($str)
172 {
173 if (is_array($str))
174 {
175 $new_array = array();
176 foreach ($str as $key => $val)
177 {
178 $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
179 }
180 return $new_array;
181 }
182
183 // We strip slashes if magic quotes is on to keep things consistent
184 if (get_magic_quotes_gpc())
185 {
186 $str = stripslashes($str);
187 }
188
189 // Should we filter the input data?
190 if ($this->use_xss_clean === TRUE)
191 {
192 $str = $this->xss_clean($str);
193 }
194
195 // Standardize newlines
196 if (strpos($str, "\r") !== FALSE)
197 {
198 $str = str_replace(array("\r\n", "\r"), "\n", $str);
199 }
200
201 return $str;
202 }
203
204 // --------------------------------------------------------------------
205
206 /**
207 * Clean Keys
208 *
209 * This is a helper function. To prevent malicious users
210 * from trying to exploit keys we make sure that keys are
211 * only named with alpha-numeric text and a few other items.
212 *
213 * @access private
214 * @param string
215 * @return string
216 */
217 function _clean_input_keys($str)
218 {
219 if ( ! preg_match("/^[a-z0-9:_\/-]+$/i", $str))
220 {
221 exit('Disallowed Key Characters.');
222 }
223
224 return $str;
225 }
226
227 // --------------------------------------------------------------------
228
229 /**
230 * Fetch from array
231 *
232 * This is a helper function to retrieve values from global arrays
233 *
234 * @access private
235 * @param array
236 * @param string
237 * @param bool
238 * @return string
239 */
240 function _fetch_from_array(&$array, $index = '', $xss_clean = FALSE)
241 {
242 if ( ! isset($array[$index]))
243 {
244 return FALSE;
245 }
246
247 if ($xss_clean === TRUE)
248 {
249 return $this->xss_clean($array[$index]);
250 }
251
252 return $array[$index];
253 }
254
255 // --------------------------------------------------------------------
256
257 /**
258 * Fetch an item from the GET array
259 *
260 * @access public
261 * @param string
262 * @param bool
263 * @return string
264 */
265 function get($index = '', $xss_clean = FALSE)
266 {
267 return $this->_fetch_from_array($_GET, $index, $xss_clean);
268 }
269
270 // --------------------------------------------------------------------
271
272 /**
273 * Fetch an item from the POST array
274 *
275 * @access public
276 * @param string
277 * @param bool
278 * @return string
279 */
280 function post($index = '', $xss_clean = FALSE)
281 {
282 return $this->_fetch_from_array($_POST, $index, $xss_clean);
283 }
284
285 // --------------------------------------------------------------------
286
287 /**
288 * Fetch an item from either the GET array or the POST
289 *
290 * @access public
291 * @param string The index key
292 * @param bool XSS cleaning
293 * @return string
294 */
295 function get_post($index = '', $xss_clean = FALSE)
296 {
297 if ( ! isset($_POST[$index]) )
298 {
299 return $this->get($index, $xss_clean);
300 }
301 else
302 {
303 return $this->post($index, $xss_clean);
304 }
305 }
306
307 // --------------------------------------------------------------------
308
309 /**
310 * Fetch an item from the COOKIE array
311 *
312 * @access public
313 * @param string
314 * @param bool
315 * @return string
316 */
317 function cookie($index = '', $xss_clean = FALSE)
318 {
319 return $this->_fetch_from_array($_COOKIE, $index, $xss_clean);
320 }
321
322 // --------------------------------------------------------------------
323
324 /**
325 * Fetch an item from the SERVER array
326 *
327 * @access public
328 * @param string
329 * @param bool
330 * @return string
331 */
332 function server($index = '', $xss_clean = FALSE)
333 {
334 return $this->_fetch_from_array($_SERVER, $index, $xss_clean);
335 }
336
337 // --------------------------------------------------------------------
338
339 /**
340 * Fetch the IP Address
341 *
342 * @access public
343 * @return string
344 */
345 function ip_address()
346 {
347 if ($this->ip_address !== FALSE)
348 {
349 return $this->ip_address;
350 }
351
352 if (config_item('proxy_ips') != '' && $this->server('HTTP_X_FORWARDED_FOR') && $this->server('REMOTE_ADDR'))
353 {
354 $proxies = preg_split('/[\s,]/', config_item('proxy_ips'), -1, PREG_SPLIT_NO_EMPTY);
355 $proxies = is_array($proxies) ? $proxies : array($proxies);
356
357 $this->ip_address = in_array($_SERVER['REMOTE_ADDR'], $proxies) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
358 }
359 elseif ($this->server('REMOTE_ADDR') AND $this->server('HTTP_CLIENT_IP'))
360 {
361 $this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
362 }
363 elseif ($this->server('REMOTE_ADDR'))
364 {
365 $this->ip_address = $_SERVER['REMOTE_ADDR'];
366 }
367 elseif ($this->server('HTTP_CLIENT_IP'))
368 {
369 $this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
370 }
371 elseif ($this->server('HTTP_X_FORWARDED_FOR'))
372 {
373 $this->ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
374 }
375
376 if ($this->ip_address === FALSE)
377 {
378 $this->ip_address = '0.0.0.0';
379 return $this->ip_address;
380 }
381
382 if (strstr($this->ip_address, ','))
383 {
384 $x = explode(',', $this->ip_address);
385 $this->ip_address = trim(end($x));
386 }
387
388 if ( ! $this->valid_ip($this->ip_address))
389 {
390 $this->ip_address = '0.0.0.0';
391 }
392
393 return $this->ip_address;
394 }
395
396 // --------------------------------------------------------------------
397
398 /**
399 * Validate IP Address
400 *
401 * Updated version suggested by Geert De Deckere
402 *
403 * @access public
404 * @param string
405 * @return string
406 */
407 function valid_ip($ip)
408 {
409 $ip_segments = explode('.', $ip);
410
411 // Always 4 segments needed
412 if (count($ip_segments) != 4)
413 {
414 return FALSE;
415 }
416 // IP can not start with 0
417 if ($ip_segments[0][0] == '0')
418 {
419 return FALSE;
420 }
421 // Check each segment
422 foreach ($ip_segments as $segment)
423 {
424 // IP segments must be digits and can not be
425 // longer than 3 digits or greater then 255
426 if ($segment == '' OR preg_match("/[^0-9]/", $segment) OR $segment > 255 OR strlen($segment) > 3)
427 {
428 return FALSE;
429 }
430 }
431
432 return TRUE;
433 }
434
435 // --------------------------------------------------------------------
436
437 /**
438 * User Agent
439 *
440 * @access public
441 * @return string
442 */
443 function user_agent()
444 {
445 if ($this->user_agent !== FALSE)
446 {
447 return $this->user_agent;
448 }
449
450 $this->user_agent = ( ! isset($_SERVER['HTTP_USER_AGENT'])) ? FALSE : $_SERVER['HTTP_USER_AGENT'];
451
452 return $this->user_agent;
453 }
454
455 // --------------------------------------------------------------------
456
457 /**
458 * Filename Security
459 *
460 * @access public
461 * @param string
462 * @return string
463 */
464 function filename_security($str)
465 {
466 $bad = array(
467 "../",
468 "./",
469 "<!--",
470 "-->",
471 "<",
472 ">",
473 "'",
474 '"',
475 '&',
476 '$',
477 '#',
478 '{',
479 '}',
480 '[',
481 ']',
482 '=',
483 ';',
484 '?',
485 "%20",
486 "%22",
487 "%3c", // <
488 "%253c", // <
489 "%3e", // >
490 "%0e", // >
491 "%28", // (
492 "%29", // )
493 "%2528", // (
494 "%26", // &
495 "%24", // $
496 "%3f", // ?
497 "%3b", // ;
498 "%3d" // =
499 );
500
501 return stripslashes(str_replace($bad, '', $str));
502 }
503
504 // --------------------------------------------------------------------
505
506 /**
507 * XSS Clean
508 *
509 * Sanitizes data so that Cross Site Scripting Hacks can be
510 * prevented. This function does a fair amount of work but
511 * it is extremely thorough, designed to prevent even the
512 * most obscure XSS attempts. Nothing is ever 100% foolproof,
513 * of course, but I haven't been able to get anything passed
514 * the filter.
515 *
516 * Note: This function should only be used to deal with data
517 * upon submission. It's not something that should
518 * be used for general runtime processing.
519 *
520 * This function was based in part on some code and ideas I
521 * got from Bitflux: http://blog.bitflux.ch/wiki/XSS_Prevention
522 *
523 * To help develop this script I used this great list of
524 * vulnerabilities along with a few other hacks I've
525 * harvested from examining vulnerabilities in other programs:
526 * http://ha.ckers.org/xss.html
527 *
528 * @access public
529 * @param string
530 * @return string
531 */
532 function xss_clean($str, $is_image = FALSE)
533 {
534 /*
535 * Is the string an array?
536 *
537 */
538 if (is_array($str))
539 {
540 while (list($key) = each($str))
541 {
542 $str[$key] = $this->xss_clean($str[$key]);
543 }
544
545 return $str;
546 }
547
548 /*
549 * Remove Invisible Characters
550 */
551 $str = $this->_remove_invisible_characters($str);
552
553 /*
554 * Protect GET variables in URLs
555 */
556
557 // 901119URL5918AMP18930PROTECT8198
558
559 $str = preg_replace('|\&([a-z\_0-9]+)\=([a-z\_0-9]+)|i', $this->xss_hash()."\\1=\\2", $str);
560
561 /*
562 * Validate standard character entities
563 *
564 * Add a semicolon if missing. We do this to enable
565 * the conversion of entities to ASCII later.
566 *
567 */
568 $str = preg_replace('#(&\#[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str);
569
570 /*
571 * Validate UTF16 two byte encoding (x00)
572 *
573 * Just as above, adds a semicolon if missing.
574 *
575 */
576 $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str);
577
578 /*
579 * Un-Protect GET variables in URLs
580 */
581 $str = str_replace($this->xss_hash(), '&', $str);
582
583 /*
584 * URL Decode
585 *
586 * Just in case stuff like this is submitted:
587 *
588 * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
589 *
590 * Note: Use rawurldecode() so it does not remove plus signs
591 *
592 */
593 //$str = rawurldecode($str);
594
595 /*
596 * Convert character entities to ASCII
597 *
598 * This permits our tests below to work reliably.
599 * We only convert entities that are within tags since
600 * these are the ones that will pose security problems.
601 *
602 */
603
604 $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
605
606 $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_html_entity_decode_callback'), $str);
607
608 /*
609 * Remove Invisible Characters Again!
610 */
611 $str = $this->_remove_invisible_characters($str);
612
613 /*
614 * Convert all tabs to spaces
615 *
616 * This prevents strings like this: ja vascript
617 * NOTE: we deal with spaces between characters later.
618 * NOTE: preg_replace was found to be amazingly slow here on large blocks of data,
619 * so we use str_replace.
620 *
621 */
622
623 if (strpos($str, "\t") !== FALSE)
624 {
625 $str = str_replace("\t", ' ', $str);
626 }
627
628 /*
629 * Capture converted string for later comparison
630 */
631 $converted_string = $str;
632
633 /*
634 * Not Allowed Under Any Conditions
635 */
636
637 foreach ($this->never_allowed_str as $key => $val)
638 {
639 $str = str_replace($key, $val, $str);
640 }
641
642 foreach ($this->never_allowed_regex as $key => $val)
643 {
644 $str = preg_replace("#".$key."#i", $val, $str);
645 }
646
647 /*
648 * Makes PHP tags safe
649 *
650 * Note: XML tags are inadvertently replaced too:
651 *
652 * <?xml
653 *
654 * But it doesn't seem to pose a problem.
655 *
656 */
657 if ($is_image === TRUE)
658 {
659 // Images have a tendency to have the PHP short opening and closing tags every so often
660 // so we skip those and only do the long opening tags.
661 $str = preg_replace('/<\?(php)/i', "<?\\1", $str);
662 }
663 else
664 {
665 $str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $str);
666 }
667
668 /*
669 * Compact any exploded words
670 *
671 * This corrects words like: j a v a s c r i p t
672 * These words are compacted back to their correct state.
673 *
674 */
675 $words = array('javascript', 'expression', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window');
676 foreach ($words as $word)
677 {
678 $temp = '';
679
680 for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++)
681 {
682 $temp .= substr($word, $i, 1)."\s*";
683 }
684
685 // We only want to do this when it is followed by a non-word character
686 // That way valid stuff like "dealer to" does not become "dealerto"
687 $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
688 }
689
690 /*
691 * Remove disallowed Javascript in links or img tags
692 * We used to do some version comparisons and use of stripos for PHP5, but it is dog slow compared
693 * to these simplified non-capturing preg_match(), especially if the pattern exists in the string
694 */
695 do
696 {
697 $original = $str;
698
699 if (preg_match("/<a/i", $str))
700 {
701 $str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", array($this, '_js_link_removal'), $str);
702 }
703
704 if (preg_match("/<img/i", $str))
705 {
706 $str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str);
707 }
708
709 if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str))
710 {
711 $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str);
712 }
713 }
714 while($original != $str);
715
716 unset($original);
717
718 /*
719 * Remove JavaScript Event Handlers
720 *
721 * Note: This code is a little blunt. It removes
722 * the event handler and anything up to the closing >,
723 * but it's unlikely to be a problem.
724 *
725 */
726 $event_handlers = array('[^a-z_\-]on\w*','xmlns');
727
728 if ($is_image === TRUE)
729 {
730 /*
731 * Adobe Photoshop puts XML metadata into JFIF images, including namespacing,
732 * so we have to allow this for images. -Paul
733 */
734 unset($event_handlers[array_search('xmlns', $event_handlers)]);
735 }
736
737 $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str);
738
739 /*
740 * Sanitize naughty HTML elements
741 *
742 * If a tag containing any of the words in the list
743 * below is found, the tag gets converted to entities.
744 *
745 * So this: <blink>
746 * Becomes: <blink>
747 *
748 */
749 $naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss';
750 $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str);
751
752 /*
753 * Sanitize naughty scripting elements
754 *
755 * Similar to above, only instead of looking for
756 * tags it looks for PHP and JavaScript commands
757 * that are disallowed. Rather than removing the
758 * code, it simply converts the parenthesis to entities
759 * rendering the code un-executable.
760 *
761 * For example: eval('some code')
762 * Becomes: eval('some code')
763 *
764 */
765 $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str);
766
767 /*
768 * Final clean up
769 *
770 * This adds a bit of extra precaution in case
771 * something got through the above filters
772 *
773 */
774 foreach ($this->never_allowed_str as $key => $val)
775 {
776 $str = str_replace($key, $val, $str);
777 }
778
779 foreach ($this->never_allowed_regex as $key => $val)
780 {
781 $str = preg_replace("#".$key."#i", $val, $str);
782 }
783
784 /*
785 * Images are Handled in a Special Way
786 * - Essentially, we want to know that after all of the character conversion is done whether
787 * any unwanted, likely XSS, code was found. If not, we return TRUE, as the image is clean.
788 * However, if the string post-conversion does not matched the string post-removal of XSS,
789 * then it fails, as there was unwanted XSS code found and removed/changed during processing.
790 */
791
792 if ($is_image === TRUE)
793 {
794 if ($str == $converted_string)
795 {
796 return TRUE;
797 }
798 else
799 {
800 return FALSE;
801 }
802 }
803
804 return $str;
805 }
806
807 // --------------------------------------------------------------------
808
809 /**
810 * Random Hash for protecting URLs
811 *
812 * @access public
813 * @return string
814 */
815 function xss_hash()
816 {
817 if ($this->xss_hash == '')
818 {
819 if (phpversion() >= 4.2)
820 mt_srand();
821 else
822 mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff);
823
824 $this->xss_hash = md5(time() + mt_rand(0, 1999999999));
825 }
826
827 return $this->xss_hash;
828 }
829
830 // --------------------------------------------------------------------
831
832 /**
833 * Remove Invisible Characters
834 *
835 * This prevents sandwiching null characters
836 * between ascii characters, like Java\0script.
837 *
838 * @access public
839 * @param string
840 * @return string
841 */
842 function _remove_invisible_characters($str)
843 {
844 static $non_displayables;
845
846 if ( ! isset($non_displayables))
847 {
848 // every control character except newline (dec 10), carriage return (dec 13), and horizontal tab (dec 09),
849 $non_displayables = array(
850 '/%0[0-8bcef]/', // url encoded 00-08, 11, 12, 14, 15
851 '/%1[0-9a-f]/', // url encoded 16-31
852 '/[\x00-\x08]/', // 00-08
853 '/\x0b/', '/\x0c/', // 11, 12
854 '/[\x0e-\x1f]/' // 14-31
855 );
856 }
857
858 do
859 {
860 $cleaned = $str;
861 $str = preg_replace($non_displayables, '', $str);
862 }
863 while ($cleaned != $str);
864
865 return $str;
866 }
867
868 // --------------------------------------------------------------------
869
870 /**
871 * Compact Exploded Words
872 *
873 * Callback function for xss_clean() to remove whitespace from
874 * things like j a v a s c r i p t
875 *
876 * @access public
877 * @param type
878 * @return type
879 */
880 function _compact_exploded_words($matches)
881 {
882 return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
883 }
884
885 // --------------------------------------------------------------------
886
887 /**
888 * Sanitize Naughty HTML
889 *
890 * Callback function for xss_clean() to remove naughty HTML elements
891 *
892 * @access private
893 * @param array
894 * @return string
895 */
896 function _sanitize_naughty_html($matches)
897 {
898 // encode opening brace
899 $str = '<'.$matches[1].$matches[2].$matches[3];
900
901 // encode captured opening or closing brace to prevent recursive vectors
902 $str .= str_replace(array('>', '<'), array('>', '<'), $matches[4]);
903
904 return $str;
905 }
906
907 // --------------------------------------------------------------------
908
909 /**
910 * JS Link Removal
911 *
912 * Callback function for xss_clean() to sanitize links
913 * This limits the PCRE backtracks, making it more performance friendly
914 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
915 * PHP 5.2+ on link-heavy strings
916 *
917 * @access private
918 * @param array
919 * @return string
920 */
921 function _js_link_removal($match)
922 {
923 $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));
924 return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|javascript\s*&colon|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
925 }
926
927 /**
928 * JS Image Removal
929 *
930 * Callback function for xss_clean() to sanitize image tags
931 * This limits the PCRE backtracks, making it more performance friendly
932 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
933 * PHP 5.2+ on image tag heavy strings
934 *
935 * @access private
936 * @param array
937 * @return string
938 */
939 function _js_img_removal($match)
940 {
941 $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));
942 return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|javascript\s*&colon|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
943 }
944
945 // --------------------------------------------------------------------
946
947 /**
948 * Attribute Conversion
949 *
950 * Used as a callback for XSS Clean
951 *
952 * @access public
953 * @param array
954 * @return string
955 */
956 function _convert_attribute($match)
957 {
958 return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]);
959 }
960
961 // --------------------------------------------------------------------
962
963 /**
964 * HTML Entity Decode Callback
965 *
966 * Used as a callback for XSS Clean
967 *
968 * @access public
969 * @param array
970 * @return string
971 */
972 function _html_entity_decode_callback($match)
973 {
974 $charset = $this->charset;
975
976 return $this->_html_entity_decode($match[0], strtoupper($charset));
977 }
978
979 // --------------------------------------------------------------------
980
981 /**
982 * HTML Entities Decode
983 *
984 * This function is a replacement for html_entity_decode()
985 *
986 * In some versions of PHP the native function does not work
987 * when UTF-8 is the specified character set, so this gives us
988 * a work-around. More info here:
989 * http://bugs.php.net/bug.php?id=25670
990 *
991 * @access private
992 * @param string
993 * @param string
994 * @return string
995 */
996 /* -------------------------------------------------
997 /* Replacement for html_entity_decode()
998 /* -------------------------------------------------*/
999
1000 /*
1001 NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the
1002 character set, and the PHP developers said they were not back porting the
1003 fix to versions other than PHP 5.x.
1004 */
1005 function _html_entity_decode($str, $charset='UTF-8')
1006 {
1007 if (stristr($str, '&') === FALSE) return $str;
1008
1009 // The reason we are not using html_entity_decode() by itself is because
1010 // while it is not technically correct to leave out the semicolon
1011 // at the end of an entity most browsers will still interpret the entity
1012 // correctly. html_entity_decode() does not convert entities without
1013 // semicolons, so we are left with our own little solution here. Bummer.
1014
1015 if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR version_compare(phpversion(), '5.0.0', '>=')))
1016 {
1017 $str = @html_entity_decode($str, ENT_COMPAT, $charset);
1018 // $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str);
1019 $str = preg_replace_callback('~&#x(0*[0-9a-f]{2,5})~i', '_chrhexdec', $str);
1020 // return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str);
1021 return preg_replace_callback('~&#([0-9]{2,4})~', '_chr', $str);
1022 }
1023
1024 // Numeric Entities
1025 // $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str);
1026 $str = preg_replace_callback('~&#x(0*[0-9a-f]{2,5});{0,1}~i', '_chrhexdec', $str);
1027 // $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str);
1028 $str = preg_replace_callback('~&#([0-9]{2,4});{0,1}~', '_chr', $str);
1029
1030 // Literal Entities - Slightly slow so we do another check
1031 if (stristr($str, '&') === FALSE)
1032 {
1033 $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES)));
1034 }
1035
1036 return $str;
1037 }
1038
1039 // --------------------------------------------------------------------
1040
1041 /**
1042 * Filter Attributes
1043 *
1044 * Filters tag attributes for consistency and safety
1045 *
1046 * @access public
1047 * @param string
1048 * @return string
1049 */
1050 function _filter_attributes($str)
1051 {
1052 $out = '';
1053
1054 if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
1055 {
1056 foreach ($matches[0] as $match)
1057 {
1058 $out .= preg_replace("#/\*.*?\*/#s", '', $match);
1059 }
1060 }
1061
1062 return $out;
1063 }
1064
1065 // --------------------------------------------------------------------
1066
1067 }
1068
1069
1070 /* callback function added for use in _html_entity_decode */
1071 function _chr($m){
1072 return chr($m[1]);
1073 }
1074
1075 /* callback function added for use in _html_entity_decode */
1076 function _chrhexdec($m){
1077 return chr(hexdec($m[1]));
1078 }
2 /**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP 4.3.2 or newer
6 *
7 * @package CodeIgniter
8 * @author ExpressionEngine Dev Team
9 * @copyright Copyright (c) 2008 - 2009, EllisLab, Inc.
10 * @license http://codeigniter.com/user_guide/license.html
11 * @link http://codeigniter.com
12 * @since Version 1.0
13 * @filesource
14 */
15
16 // ------------------------------------------------------------------------
17
18 /**
19 * Input Class
20 *
21 * Pre-processes global input data for security
22 *
23 * @package CodeIgniter
24 * @subpackage Libraries
25 * @category Input
26 * @author ExpressionEngine Dev Team
27 * @link http://codeigniter.com/user_guide/libraries/input.html
28 */
29 class CI_Input {
30 var $use_xss_clean = FALSE;
31 var $xss_hash = '';
32 var $ip_address = FALSE;
33 var $user_agent = FALSE;
34 var $allow_get_array = FALSE;
35 var $charset = 'iso-8859-1';
36
37 /* never allowed, string replacement */
38 var $never_allowed_str = array(
39 'document.cookie' => '[removed]',
40 'document.write' => '[removed]',
41 '.parentNode' => '[removed]',
42 '.innerHTML' => '[removed]',
43 'window.location' => '[removed]',
44 '-moz-binding' => '[removed]',
45 '<!--' => '<!--',
46 '-->' => '-->',
47 '<![CDATA[' => '<![CDATA['
48 );
49 /* never allowed, regex replacement */
50 var $never_allowed_regex = array(
51 "javascript\s*:" => '[removed]',
52 "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE
53 "vbscript\s*:" => '[removed]', // IE, surprise!
54 "Redirect\s+302" => '[removed]'
55 );
56
57
58 function __construct(){
59 $this->CI_Input();
60 }
61
62 /**
63 * Constructor
64 *
65 * Sets whether to globally enable the XSS processing
66 * and whether to allow the $_GET array
67 *
68 * @access public
69 */
70 function CI_Input()
71 {
72 $this->use_xss_clean = FALSE;
73 $this->allow_get_array = TRUE;
74 // $this->_sanitize_globals();
75 }
76
77 // --------------------------------------------------------------------
78
79 /**
80 * Sanitize Globals
81 *
82 * This function does the following:
83 *
84 * Unsets $_GET data (if query strings are not enabled)
85 *
86 * Unsets all globals if register_globals is enabled
87 *
88 * Standardizes newline characters to \n
89 *
90 * @access private
91 * @return void
92 */
93 function _sanitize_globals()
94 {
95 // Would kind of be "wrong" to unset any of these GLOBALS
96 $protected = array('_SERVER', '_GET', '_POST', '_FILES', '_REQUEST', '_SESSION', '_ENV', 'GLOBALS', 'HTTP_RAW_POST_DATA',
97 'system_folder', 'application_folder', 'BM', 'EXT', 'CFG', 'URI', 'RTR', 'OUT', 'IN');
98
99 // Unset globals for security.
100 // This is effectively the same as register_globals = off
101 foreach (array($_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, $_ENV, (isset($_SESSION) && is_array($_SESSION)) ? $_SESSION : array()) as $global)
102 {
103 if ( ! is_array($global))
104 {
105 if ( ! in_array($global, $protected))
106 {
107 unset($GLOBALS[$global]);
108 }
109 }
110 else
111 {
112 foreach ($global as $key => $val)
113 {
114 if ( ! in_array($key, $protected))
115 {
116 unset($GLOBALS[$key]);
117 }
118
119 if (is_array($val))
120 {
121 foreach($val as $k => $v)
122 {
123 if ( ! in_array($k, $protected))
124 {
125 unset($GLOBALS[$k]);
126 }
127 }
128 }
129 }
130 }
131 }
132
133 // Is $_GET data allowed? If not we'll set the $_GET to an empty array
134 if ($this->allow_get_array == FALSE)
135 {
136 $_GET = array();
137 }
138 else
139 {
140 $_GET = $this->_clean_input_data($_GET);
141 }
142
143 // Clean $_POST Data
144 $_POST = $this->_clean_input_data($_POST);
145
146 // Clean $_COOKIE Data
147 // Also get rid of specially treated cookies that might be set by a server
148 // or silly application, that are of no use to a CI application anyway
149 // but that when present will trip our 'Disallowed Key Characters' alarm
150 // http://www.ietf.org/rfc/rfc2109.txt
151 // note that the key names below are single quoted strings, and are not PHP variables
152 unset($_COOKIE['$Version']);
153 unset($_COOKIE['$Path']);
154 unset($_COOKIE['$Domain']);
155 $_COOKIE = $this->_clean_input_data($_COOKIE);
156
157 }
158
159 // --------------------------------------------------------------------
160
161 /**
162 * Clean Input Data
163 *
164 * This is a helper function. It escapes data and
165 * standardizes newline characters to \n
166 *
167 * @access private
168 * @param string
169 * @return string
170 */
171 function _clean_input_data($str)
172 {
173 if (is_array($str))
174 {
175 $new_array = array();
176 foreach ($str as $key => $val)
177 {
178 $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
179 }
180 return $new_array;
181 }
182
183 // We strip slashes if magic quotes is on to keep things consistent
184 if (get_magic_quotes_gpc())
185 {
186 $str = stripslashes($str);
187 }
188
189 // Should we filter the input data?
190 if ($this->use_xss_clean === TRUE)
191 {
192 $str = $this->xss_clean($str);
193 }
194
195 // Standardize newlines
196 if (strpos($str, "\r") !== FALSE)
197 {
198 $str = str_replace(array("\r\n", "\r"), "\n", $str);
199 }
200
201 return $str;
202 }
203
204 // --------------------------------------------------------------------
205
206 /**
207 * Clean Keys
208 *
209 * This is a helper function. To prevent malicious users
210 * from trying to exploit keys we make sure that keys are
211 * only named with alpha-numeric text and a few other items.
212 *
213 * @access private
214 * @param string
215 * @return string
216 */
217 function _clean_input_keys($str)
218 {
219 if ( ! preg_match("/^[a-z0-9:_\/-]+$/i", $str))
220 {
221 exit('Disallowed Key Characters.');
222 }
223
224 return $str;
225 }
226
227 // --------------------------------------------------------------------
228
229 /**
230 * Fetch from array
231 *
232 * This is a helper function to retrieve values from global arrays
233 *
234 * @access private
235 * @param array
236 * @param string
237 * @param bool
238 * @return string
239 */
240 function _fetch_from_array(&$array, $index = '', $xss_clean = FALSE)
241 {
242 if ( ! isset($array[$index]))
243 {
244 return FALSE;
245 }
246
247 if ($xss_clean === TRUE)
248 {
249 return $this->xss_clean($array[$index]);
250 }
251
252 return $array[$index];
253 }
254
255 // --------------------------------------------------------------------
256
257 /**
258 * Fetch an item from the GET array
259 *
260 * @access public
261 * @param string
262 * @param bool
263 * @return string
264 */
265 function get($index = '', $xss_clean = FALSE)
266 {
267 return $this->_fetch_from_array($_GET, $index, $xss_clean);
268 }
269
270 // --------------------------------------------------------------------
271
272 /**
273 * Fetch an item from the POST array
274 *
275 * @access public
276 * @param string
277 * @param bool
278 * @return string
279 */
280 function post($index = '', $xss_clean = FALSE)
281 {
282 return $this->_fetch_from_array($_POST, $index, $xss_clean);
283 }
284
285 // --------------------------------------------------------------------
286
287 /**
288 * Fetch an item from either the GET array or the POST
289 *
290 * @access public
291 * @param string The index key
292 * @param bool XSS cleaning
293 * @return string
294 */
295 function get_post($index = '', $xss_clean = FALSE)
296 {
297 if ( ! isset($_POST[$index]) )
298 {
299 return $this->get($index, $xss_clean);
300 }
301 else
302 {
303 return $this->post($index, $xss_clean);
304 }
305 }
306
307 // --------------------------------------------------------------------
308
309 /**
310 * Fetch an item from the COOKIE array
311 *
312 * @access public
313 * @param string
314 * @param bool
315 * @return string
316 */
317 function cookie($index = '', $xss_clean = FALSE)
318 {
319 return $this->_fetch_from_array($_COOKIE, $index, $xss_clean);
320 }
321
322 // --------------------------------------------------------------------
323
324 /**
325 * Fetch an item from the SERVER array
326 *
327 * @access public
328 * @param string
329 * @param bool
330 * @return string
331 */
332 function server($index = '', $xss_clean = FALSE)
333 {
334 return $this->_fetch_from_array($_SERVER, $index, $xss_clean);
335 }
336
337 // --------------------------------------------------------------------
338
339 /**
340 * Fetch the IP Address
341 *
342 * @access public
343 * @return string
344 */
345 function ip_address()
346 {
347 if ($this->ip_address !== FALSE)
348 {
349 return $this->ip_address;
350 }
351
352 if (config_item('proxy_ips') != '' && $this->server('HTTP_X_FORWARDED_FOR') && $this->server('REMOTE_ADDR'))
353 {
354 $proxies = preg_split('/[\s,]/', config_item('proxy_ips'), -1, PREG_SPLIT_NO_EMPTY);
355 $proxies = is_array($proxies) ? $proxies : array($proxies);
356
357 $this->ip_address = in_array($_SERVER['REMOTE_ADDR'], $proxies) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
358 }
359 elseif ($this->server('REMOTE_ADDR') AND $this->server('HTTP_CLIENT_IP'))
360 {
361 $this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
362 }
363 elseif ($this->server('REMOTE_ADDR'))
364 {
365 $this->ip_address = $_SERVER['REMOTE_ADDR'];
366 }
367 elseif ($this->server('HTTP_CLIENT_IP'))
368 {
369 $this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
370 }
371 elseif ($this->server('HTTP_X_FORWARDED_FOR'))
372 {
373 $this->ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
374 }
375
376 if ($this->ip_address === FALSE)
377 {
378 $this->ip_address = '0.0.0.0';
379 return $this->ip_address;
380 }
381
382 if (strstr($this->ip_address, ','))
383 {
384 $x = explode(',', $this->ip_address);
385 $this->ip_address = trim(end($x));
386 }
387
388 if ( ! $this->valid_ip($this->ip_address))
389 {
390 $this->ip_address = '0.0.0.0';
391 }
392
393 return $this->ip_address;
394 }
395
396 // --------------------------------------------------------------------
397
398 /**
399 * Validate IP Address
400 *
401 * Updated version suggested by Geert De Deckere
402 *
403 * @access public
404 * @param string
405 * @return string
406 */
407 function valid_ip($ip)
408 {
409 $ip_segments = explode('.', $ip);
410
411 // Always 4 segments needed
412 if (count($ip_segments) != 4)
413 {
414 return FALSE;
415 }
416 // IP can not start with 0
417 if ($ip_segments[0][0] == '0')
418 {
419 return FALSE;
420 }
421 // Check each segment
422 foreach ($ip_segments as $segment)
423 {
424 // IP segments must be digits and can not be
425 // longer than 3 digits or greater then 255
426 if ($segment == '' OR preg_match("/[^0-9]/", $segment) OR $segment > 255 OR strlen($segment) > 3)
427 {
428 return FALSE;
429 }
430 }
431
432 return TRUE;
433 }
434
435 // --------------------------------------------------------------------
436
437 /**
438 * User Agent
439 *
440 * @access public
441 * @return string
442 */
443 function user_agent()
444 {
445 if ($this->user_agent !== FALSE)
446 {
447 return $this->user_agent;
448 }
449
450 $this->user_agent = ( ! isset($_SERVER['HTTP_USER_AGENT'])) ? FALSE : $_SERVER['HTTP_USER_AGENT'];
451
452 return $this->user_agent;
453 }
454
455 // --------------------------------------------------------------------
456
457 /**
458 * Filename Security
459 *
460 * @access public
461 * @param string
462 * @return string
463 */
464 function filename_security($str)
465 {
466 $bad = array(
467 "../",
468 "./",
469 "<!--",
470 "-->",
471 "<",
472 ">",
473 "'",
474 '"',
475 '&',
476 '$',
477 '#',
478 '{',
479 '}',
480 '[',
481 ']',
482 '=',
483 ';',
484 '?',
485 "%20",
486 "%22",
487 "%3c", // <
488 "%253c", // <
489 "%3e", // >
490 "%0e", // >
491 "%28", // (
492 "%29", // )
493 "%2528", // (
494 "%26", // &
495 "%24", // $
496 "%3f", // ?
497 "%3b", // ;
498 "%3d" // =
499 );
500
501 return stripslashes(str_replace($bad, '', $str));
502 }
503
504 // --------------------------------------------------------------------
505
506 /**
507 * XSS Clean
508 *
509 * Sanitizes data so that Cross Site Scripting Hacks can be
510 * prevented. This function does a fair amount of work but
511 * it is extremely thorough, designed to prevent even the
512 * most obscure XSS attempts. Nothing is ever 100% foolproof,
513 * of course, but I haven't been able to get anything passed
514 * the filter.
515 *
516 * Note: This function should only be used to deal with data
517 * upon submission. It's not something that should
518 * be used for general runtime processing.
519 *
520 * This function was based in part on some code and ideas I
521 * got from Bitflux: http://blog.bitflux.ch/wiki/XSS_Prevention
522 *
523 * To help develop this script I used this great list of
524 * vulnerabilities along with a few other hacks I've
525 * harvested from examining vulnerabilities in other programs:
526 * http://ha.ckers.org/xss.html
527 *
528 * @access public
529 * @param string
530 * @return string
531 */
532 function xss_clean($str, $is_image = FALSE)
533 {
534 /*
535 * Is the string an array?
536 *
537 */
538 if (is_array($str))
539 {
540 while (list($key) = each($str))
541 {
542 $str[$key] = $this->xss_clean($str[$key]);
543 }
544
545 return $str;
546 }
547
548 /*
549 * Remove Invisible Characters
550 */
551 $str = $this->_remove_invisible_characters($str);
552
553 /*
554 * Protect GET variables in URLs
555 */
556
557 // 901119URL5918AMP18930PROTECT8198
558
559 $str = preg_replace('|\&([a-z\_0-9]+)\=([a-z\_0-9]+)|i', $this->xss_hash()."\\1=\\2", $str);
560
561 /*
562 * Validate standard character entities
563 *
564 * Add a semicolon if missing. We do this to enable
565 * the conversion of entities to ASCII later.
566 *
567 */
568 $str = preg_replace('#(&\#[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str);
569
570 /*
571 * Validate UTF16 two byte encoding (x00)
572 *
573 * Just as above, adds a semicolon if missing.
574 *
575 */
576 $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str);
577
578 /*
579 * Un-Protect GET variables in URLs
580 */
581 $str = str_replace($this->xss_hash(), '&', $str);
582
583 /*
584 * URL Decode
585 *
586 * Just in case stuff like this is submitted:
587 *
588 * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
589 *
590 * Note: Use rawurldecode() so it does not remove plus signs
591 *
592 */
593 //$str = rawurldecode($str);
594
595 /*
596 * Convert character entities to ASCII
597 *
598 * This permits our tests below to work reliably.
599 * We only convert entities that are within tags since
600 * these are the ones that will pose security problems.
601 *
602 */
603
604 $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
605
606 $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_html_entity_decode_callback'), $str);
607
608 /*
609 * Remove Invisible Characters Again!
610 */
611 $str = $this->_remove_invisible_characters($str);
612
613 /*
614 * Convert all tabs to spaces
615 *
616 * This prevents strings like this: ja vascript
617 * NOTE: we deal with spaces between characters later.
618 * NOTE: preg_replace was found to be amazingly slow here on large blocks of data,
619 * so we use str_replace.
620 *
621 */
622
623 if (strpos($str, "\t") !== FALSE)
624 {
625 $str = str_replace("\t", ' ', $str);
626 }
627
628 /*
629 * Capture converted string for later comparison
630 */
631 $converted_string = $str;
632
633 /*
634 * Not Allowed Under Any Conditions
635 */
636
637 foreach ($this->never_allowed_str as $key => $val)
638 {
639 $str = str_replace($key, $val, $str);
640 }
641
642 foreach ($this->never_allowed_regex as $key => $val)
643 {
644 $str = preg_replace("#".$key."#i", $val, $str);
645 }
646
647 /*
648 * Makes PHP tags safe
649 *
650 * Note: XML tags are inadvertently replaced too:
651 *
652 * <?xml
653 *
654 * But it doesn't seem to pose a problem.
655 *
656 */
657 if ($is_image === TRUE)
658 {
659 // Images have a tendency to have the PHP short opening and closing tags every so often
660 // so we skip those and only do the long opening tags.
661 $str = preg_replace('/<\?(php)/i', "<?\\1", $str);
662 }
663 else
664 {
665 $str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $str);
666 }
667
668 /*
669 * Compact any exploded words
670 *
671 * This corrects words like: j a v a s c r i p t
672 * These words are compacted back to their correct state.
673 *
674 */
675 $words = array('javascript', 'expression', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window');
676 foreach ($words as $word)
677 {
678 $temp = '';
679
680 for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++)
681 {
682 $temp .= substr($word, $i, 1)."\s*";
683 }
684
685 // We only want to do this when it is followed by a non-word character
686 // That way valid stuff like "dealer to" does not become "dealerto"
687 $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
688 }
689
690 /*
691 * Remove disallowed Javascript in links or img tags
692 * We used to do some version comparisons and use of stripos for PHP5, but it is dog slow compared
693 * to these simplified non-capturing preg_match(), especially if the pattern exists in the string
694 */
695 do
696 {
697 $original = $str;
698
699 if (preg_match("/<a/i", $str))
700 {
701 $str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", array($this, '_js_link_removal'), $str);
702 }
703
704 if (preg_match("/<img/i", $str))
705 {
706 $str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str);
707 }
708
709 if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str))
710 {
711 $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str);
712 }
713 }
714 while($original != $str);
715
716 unset($original);
717
718 /*
719 * Remove JavaScript Event Handlers
720 *
721 * Note: This code is a little blunt. It removes
722 * the event handler and anything up to the closing >,
723 * but it's unlikely to be a problem.
724 *
725 */
726 $event_handlers = array('[^a-z_\-]on\w*','xmlns');
727
728 if ($is_image === TRUE)
729 {
730 /*
731 * Adobe Photoshop puts XML metadata into JFIF images, including namespacing,
732 * so we have to allow this for images. -Paul
733 */
734 unset($event_handlers[array_search('xmlns', $event_handlers)]);
735 }
736
737 $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str);
738
739 /*
740 * Sanitize naughty HTML elements
741 *
742 * If a tag containing any of the words in the list
743 * below is found, the tag gets converted to entities.
744 *
745 * So this: <blink>
746 * Becomes: <blink>
747 *
748 */
749 $naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss';
750 $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str);
751
752 /*
753 * Sanitize naughty scripting elements
754 *
755 * Similar to above, only instead of looking for
756 * tags it looks for PHP and JavaScript commands
757 * that are disallowed. Rather than removing the
758 * code, it simply converts the parenthesis to entities
759 * rendering the code un-executable.
760 *
761 * For example: eval('some code')
762 * Becomes: eval('some code')
763 *
764 */
765 $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str);
766
767 /*
768 * Final clean up
769 *
770 * This adds a bit of extra precaution in case
771 * something got through the above filters
772 *
773 */
774 foreach ($this->never_allowed_str as $key => $val)
775 {
776 $str = str_replace($key, $val, $str);
777 }
778
779 foreach ($this->never_allowed_regex as $key => $val)
780 {
781 $str = preg_replace("#".$key."#i", $val, $str);
782 }
783
784 /*
785 * Images are Handled in a Special Way
786 * - Essentially, we want to know that after all of the character conversion is done whether
787 * any unwanted, likely XSS, code was found. If not, we return TRUE, as the image is clean.
788 * However, if the string post-conversion does not matched the string post-removal of XSS,
789 * then it fails, as there was unwanted XSS code found and removed/changed during processing.
790 */
791
792 if ($is_image === TRUE)
793 {
794 if ($str == $converted_string)
795 {
796 return TRUE;
797 }
798 else
799 {
800 return FALSE;
801 }
802 }
803
804 return $str;
805 }
806
807 // --------------------------------------------------------------------
808
809 /**
810 * Random Hash for protecting URLs
811 *
812 * @access public
813 * @return string
814 */
815 function xss_hash()
816 {
817 if ($this->xss_hash == '')
818 {
819 if (phpversion() >= 4.2)
820 mt_srand();
821 else
822 mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff);
823
824 $this->xss_hash = md5(time() + mt_rand(0, 1999999999));
825 }
826
827 return $this->xss_hash;
828 }
829
830 // --------------------------------------------------------------------
831
832 /**
833 * Remove Invisible Characters
834 *
835 * This prevents sandwiching null characters
836 * between ascii characters, like Java\0script.
837 *
838 * @access public
839 * @param string
840 * @return string
841 */
842 function _remove_invisible_characters($str)
843 {
844 static $non_displayables;
845
846 if ( ! isset($non_displayables))
847 {
848 // every control character except newline (dec 10), carriage return (dec 13), and horizontal tab (dec 09),
849 $non_displayables = array(
850 '/%0[0-8bcef]/', // url encoded 00-08, 11, 12, 14, 15
851 '/%1[0-9a-f]/', // url encoded 16-31
852 '/[\x00-\x08]/', // 00-08
853 '/\x0b/', '/\x0c/', // 11, 12
854 '/[\x0e-\x1f]/' // 14-31
855 );
856 }
857
858 do
859 {
860 $cleaned = $str;
861 $str = preg_replace($non_displayables, '', $str);
862 }
863 while ($cleaned != $str);
864
865 return $str;
866 }
867
868 // --------------------------------------------------------------------
869
870 /**
871 * Compact Exploded Words
872 *
873 * Callback function for xss_clean() to remove whitespace from
874 * things like j a v a s c r i p t
875 *
876 * @access public
877 * @param type
878 * @return type
879 */
880 function _compact_exploded_words($matches)
881 {
882 return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
883 }
884
885 // --------------------------------------------------------------------
886
887 /**
888 * Sanitize Naughty HTML
889 *
890 * Callback function for xss_clean() to remove naughty HTML elements
891 *
892 * @access private
893 * @param array
894 * @return string
895 */
896 function _sanitize_naughty_html($matches)
897 {
898 // encode opening brace
899 $str = '<'.$matches[1].$matches[2].$matches[3];
900
901 // encode captured opening or closing brace to prevent recursive vectors
902 $str .= str_replace(array('>', '<'), array('>', '<'), $matches[4]);
903
904 return $str;
905 }
906
907 // --------------------------------------------------------------------
908
909 /**
910 * JS Link Removal
911 *
912 * Callback function for xss_clean() to sanitize links
913 * This limits the PCRE backtracks, making it more performance friendly
914 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
915 * PHP 5.2+ on link-heavy strings
916 *
917 * @access private
918 * @param array
919 * @return string
920 */
921 function _js_link_removal($match)
922 {
923 $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));
924 return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|javascript\s*&colon|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
925 }
926
927 /**
928 * JS Image Removal
929 *
930 * Callback function for xss_clean() to sanitize image tags
931 * This limits the PCRE backtracks, making it more performance friendly
932 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
933 * PHP 5.2+ on image tag heavy strings
934 *
935 * @access private
936 * @param array
937 * @return string
938 */
939 function _js_img_removal($match)
940 {
941 $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));
942 return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|javascript\s*&colon|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
943 }
944
945 // --------------------------------------------------------------------
946
947 /**
948 * Attribute Conversion
949 *
950 * Used as a callback for XSS Clean
951 *
952 * @access public
953 * @param array
954 * @return string
955 */
956 function _convert_attribute($match)
957 {
958 return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]);
959 }
960
961 // --------------------------------------------------------------------
962
963 /**
964 * HTML Entity Decode Callback
965 *
966 * Used as a callback for XSS Clean
967 *
968 * @access public
969 * @param array
970 * @return string
971 */
972 function _html_entity_decode_callback($match)
973 {
974 $charset = $this->charset;
975
976 return $this->_html_entity_decode($match[0], strtoupper($charset));
977 }
978
979 // --------------------------------------------------------------------
980
981 /**
982 * HTML Entities Decode
983 *
984 * This function is a replacement for html_entity_decode()
985 *
986 * In some versions of PHP the native function does not work
987 * when UTF-8 is the specified character set, so this gives us
988 * a work-around. More info here:
989 * http://bugs.php.net/bug.php?id=25670
990 *
991 * @access private
992 * @param string
993 * @param string
994 * @return string
995 */
996 /* -------------------------------------------------
997 /* Replacement for html_entity_decode()
998 /* -------------------------------------------------*/
999
1000 /*
1001 NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the
1002 character set, and the PHP developers said they were not back porting the
1003 fix to versions other than PHP 5.x.
1004 */
1005 function _html_entity_decode($str, $charset='UTF-8')
1006 {
1007 if (stristr($str, '&') === FALSE) return $str;
1008
1009 // The reason we are not using html_entity_decode() by itself is because
1010 // while it is not technically correct to leave out the semicolon
1011 // at the end of an entity most browsers will still interpret the entity
1012 // correctly. html_entity_decode() does not convert entities without
1013 // semicolons, so we are left with our own little solution here. Bummer.
1014
1015 if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR version_compare(phpversion(), '5.0.0', '>=')))
1016 {
1017 $str = @html_entity_decode($str, ENT_COMPAT, $charset);
1018 // $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str);
1019 $str = preg_replace_callback('~&#x(0*[0-9a-f]{2,5})~i', '_chrhexdec', $str);
1020 // return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str);
1021 return preg_replace_callback('~&#([0-9]{2,4})~', '_chr', $str);
1022 }
1023
1024 // Numeric Entities
1025 // $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str);
1026 $str = preg_replace_callback('~&#x(0*[0-9a-f]{2,5});{0,1}~i', '_chrhexdec', $str);
1027 // $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str);
1028 $str = preg_replace_callback('~&#([0-9]{2,4});{0,1}~', '_chr', $str);
1029
1030 // Literal Entities - Slightly slow so we do another check
1031 if (stristr($str, '&') === FALSE)
1032 {
1033 $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES)));
1034 }
1035
1036 return $str;
1037 }
1038
1039 // --------------------------------------------------------------------
1040
1041 /**
1042 * Filter Attributes
1043 *
1044 * Filters tag attributes for consistency and safety
1045 *
1046 * @access public
1047 * @param string
1048 * @return string
1049 */
1050 function _filter_attributes($str)
1051 {
1052 $out = '';
1053
1054 if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
1055 {
1056 foreach ($matches[0] as $match)
1057 {
1058 $out .= preg_replace("#/\*.*?\*/#s", '', $match);
1059 }
1060 }
1061
1062 return $out;
1063 }
1064
1065 // --------------------------------------------------------------------
1066
1067 }
1068
1069
1070 /* callback function added for use in _html_entity_decode */
1071 function _chr($m){
1072 return chr($m[1]);
1073 }
1074
1075 /* callback function added for use in _html_entity_decode */
1076 function _chrhexdec($m){
1077 return chr(hexdec($m[1]));
1078 }